import fs from 'node:fs';
import path from 'node:path';

import * as prompts from '@clack/prompts';
import spawn from 'cross-spawn';
import mri from 'mri';
import colors from 'picocolors';

const { blue, blueBright, green, greenBright, yellow } = colors;

const argv = mri<{
  template?: string;
  help?: boolean;
  overwrite?: boolean;
}>(process.argv.slice(2), {
  alias: { h: 'help', t: 'template' },
  boolean: ['help', 'overwrite'],
  string: ['template'],
});
const cwd = process.cwd();

// prettier-ignore
const helpMessage = `\
Usage: create-egg [OPTION]... [DIRECTORY]

Create a new Egg.js project.
With no arguments, start the CLI in interactive mode.

Options:
  -t, --template NAME        use a specific template

Available templates:
${green('tegg')}                         egg@4 with tegg module
${blue('simple-ts')}                    egg@4 with vanilla TypeScript
${green('egg3-tegg')}                    egg@3 with tegg module
${blue('egg3-simple-ts')}               egg@3 with vanilla TypeScript
${yellow('egg3-simple-js')}               egg@3 with vanilla JavaScript
`;

type ColorFunc = (str: string | number) => string;
type Template = {
  name: string;
  display: string;
  color: ColorFunc;
  customCommand?: string;
};

const TEMPLATES: Template[] = [
  {
    name: 'tegg',
    display: 'Tegg starter, egg@4 with tegg module',
    color: green,
  },
  {
    name: 'simple-ts',
    display: 'Simple starter, egg@4 with vanilla TypeScript',
    color: blue,
  },
  {
    name: 'egg3-tegg',
    display: 'Tegg starter, egg@3 with tegg module',
    color: green,
  },
  {
    name: 'egg3-simple-ts',
    display: 'Simple starter, egg@3 with vanilla TypeScript',
    color: blue,
  },
  {
    name: 'egg3-simple-js',
    display: 'Simple starter, egg@3 with vanilla JavaScript',
    color: yellow,
  },
];

const defaultTargetDir = 'egg-project';

export async function init(): Promise<void> {
  const argTargetDir = argv._[0] ? formatTargetDir(String(argv._[0])) : undefined;
  const argTemplate = argv.template;
  const argOverwrite = argv.overwrite;

  const help = argv.help;
  if (help) {
    console.log(helpMessage);
    return;
  }

  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
  const cancel = () => prompts.cancel('Operation cancelled');

  prompts.intro(`${greenBright('Egg.js')} - Born to build better enterprise application and framework`);

  // 1. Get project name and target dir
  let targetDir = argTargetDir;
  if (!targetDir) {
    const projectName = await prompts.text({
      message: 'Project name:',
      defaultValue: defaultTargetDir,
      placeholder: defaultTargetDir,
      validate: (value) => {
        return value.length === 0 || formatTargetDir(value).length > 0 ? undefined : 'Invalid project name';
      },
    });
    if (prompts.isCancel(projectName)) return cancel();
    targetDir = formatTargetDir(projectName);
  }

  // 2. Handle directory if exist and not empty
  if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
    const overwrite = argOverwrite
      ? 'yes'
      : await prompts.select({
          message:
            (targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`) +
            ` is not empty. Please choose how to proceed:`,
          options: [
            {
              label: 'Cancel operation',
              value: 'no',
            },
            {
              label: 'Remove existing files and continue',
              value: 'yes',
            },
            {
              label: 'Ignore files and continue',
              value: 'ignore',
            },
          ],
        });
    if (prompts.isCancel(overwrite)) return cancel();
    switch (overwrite) {
      case 'yes':
        emptyDir(targetDir);
        break;
      case 'no':
        cancel();
        return;
    }
  }

  // 3. Get package name
  let packageName = path.basename(path.resolve(targetDir));
  if (!isValidPackageName(packageName)) {
    const packageNameResult = await prompts.text({
      message: 'Package name:',
      defaultValue: toValidPackageName(packageName),
      placeholder: toValidPackageName(packageName),
      validate(dir) {
        if (!isValidPackageName(dir)) {
          return 'Invalid package.json name';
        }
      },
    });
    if (prompts.isCancel(packageNameResult)) return cancel();
    packageName = packageNameResult;
  }

  // 4. Choose a template
  let template = argTemplate;
  let hasInvalidArgTemplate = false;
  if (argTemplate && !TEMPLATES.some((t) => t.name === argTemplate)) {
    template = undefined;
    hasInvalidArgTemplate = true;
  }
  if (!template) {
    const selectedTemplate = await prompts.select({
      message: hasInvalidArgTemplate
        ? `"${argTemplate}" isn't a valid template. Please choose from below: `
        : 'Select a template:',
      options: TEMPLATES.map((template) => {
        const templateColor = template.color;
        return {
          label: templateColor(template.display || template.name),
          value: template.name,
        };
      }),
    });
    if (prompts.isCancel(selectedTemplate)) return cancel();

    template = selectedTemplate;
  }

  const root = path.join(cwd, targetDir);
  fs.mkdirSync(root, { recursive: true });

  const pkgManager = pkgInfo ? pkgInfo.name : 'npm';

  const { customCommand } = TEMPLATES.find((t) => t.name === template) ?? {};

  if (customCommand) {
    const fullCustomCommand = getFullCustomCommand(customCommand, pkgInfo);

    const [command, ...args] = fullCustomCommand.split(' ');
    // we replace TARGET_DIR here because targetDir may include a space
    const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', () => targetDir));
    const { status } = spawn.sync(command, replacedArgs, {
      stdio: 'inherit',
    });
    process.exit(status ?? 0);
  }

  prompts.log.step(`Scaffolding project with ${blueBright(template)} in ${root}...`);

  const templateDir = path.join(import.meta.dirname, `templates/${template}`);

  const write = (file: string, content?: string) => {
    const targetPath = path.join(root, file.startsWith('_') ? file.slice(1) : file);
    if (content) {
      fs.writeFileSync(targetPath, content);
    } else {
      copy(path.join(templateDir, file), targetPath);
    }
  };

  const files = fs.readdirSync(templateDir);
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file);
  }

  let pkgJsonContent = fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8');
  pkgJsonContent = pkgJsonContent.replaceAll('{{name}}', packageName);
  const pkg = JSON.parse(pkgJsonContent);

  // set packageManager
  if (pkgInfo) {
    pkg.packageManager = `${pkgInfo.name}@${pkgInfo.version}`;
  }

  write('package.json', JSON.stringify(pkg, null, 2) + '\n');

  const cdProjectName = path.relative(cwd, root);

  // 5. Run git init if user choose to run git init
  const runGitInit = await prompts.confirm({
    message: 'Initialize git repository?',
    initialValue: true,
  });
  if (runGitInit) {
    spawn.sync('git', ['init', cdProjectName], { stdio: 'pipe' });
    prompts.log.success('Git repository initialized');
  }

  let doneMessage = '';
  doneMessage += `Done. Now run:\n`;
  if (root !== cwd) {
    doneMessage += `\n  cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`;
  }
  switch (pkgManager) {
    case 'yarn':
      doneMessage += '\n  yarn';
      doneMessage += '\n  yarn dev';
      break;
    default:
      doneMessage += `\n  ${pkgManager} install`;
      doneMessage += `\n  ${pkgManager} run dev`;
      break;
  }
  prompts.outro(greenBright(doneMessage));
}

function formatTargetDir(targetDir: string) {
  return targetDir.trim().replace(/\/+$/g, '');
}

function copy(src: string, dest: string) {
  const stat = fs.statSync(src);
  if (stat.isDirectory()) {
    copyDir(src, dest);
  } else {
    fs.copyFileSync(src, dest);
  }
}

function isValidPackageName(projectName: string) {
  return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(projectName);
}

function toValidPackageName(projectName: string) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z\d\-~]+/g, '-');
}

function copyDir(srcDir: string, destDir: string) {
  fs.mkdirSync(destDir, { recursive: true });
  for (const file of fs.readdirSync(srcDir)) {
    const srcFile = path.resolve(srcDir, file);
    const destFile = path.resolve(destDir, file);
    copy(srcFile, destFile);
  }
}

function isEmpty(path: string) {
  const files = fs.readdirSync(path);
  return files.length === 0 || (files.length === 1 && files[0] === '.git');
}

function emptyDir(dir: string) {
  if (!fs.existsSync(dir)) {
    return;
  }
  for (const file of fs.readdirSync(dir)) {
    if (file === '.git') {
      continue;
    }
    fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
  }
}

interface PkgInfo {
  name: string;
  version: string;
}

function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined {
  if (!userAgent) return undefined;
  const pkgSpec = userAgent.split(' ')[0];
  const pkgSpecArr = pkgSpec.split('/');
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1],
  };
}

// function editFile(file: string, callback: (content: string) => string) {
//   const content = fs.readFileSync(file, 'utf-8')
//   fs.writeFileSync(file, callback(content), 'utf-8')
// }

function getFullCustomCommand(customCommand: string, pkgInfo?: PkgInfo) {
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
  const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.');

  return (
    customCommand
      .replace(/^npm create (?:-- )?/, () => {
        // `bun create` uses it's own set of templates,
        // the closest alternative is using `bun x` directly on the package
        if (pkgManager === 'bun') {
          return 'bun x create-';
        }
        // pnpm doesn't support the -- syntax
        if (pkgManager === 'pnpm') {
          return 'pnpm create ';
        }
        // For other package managers, preserve the original format
        return customCommand.startsWith('npm create -- ') ? `${pkgManager} create -- ` : `${pkgManager} create `;
      })
      // Only Yarn 1.x doesn't support `@version` in the `create` command
      .replace('@latest', () => (isYarn1 ? '' : '@latest'))
      .replace(/^npm exec/, () => {
        // Prefer `pnpm dlx`, `yarn dlx`, or `bun x`
        if (pkgManager === 'pnpm') {
          return 'pnpm dlx';
        }
        if (pkgManager === 'yarn' && !isYarn1) {
          return 'yarn dlx';
        }
        if (pkgManager === 'bun') {
          return 'bun x';
        }
        // Use `npm exec` in all other cases,
        // including Yarn 1.x and other custom npm clients.
        return 'npm exec';
      })
  );
}
